30. 优先使用泛型方法

      编写泛型方法类似于编写泛型类型。 考虑这个方法,它返回两个集合的并集:

      此方法可以编译但有两个警告:

    1. Union.java:5: warning: [unchecked] unchecked call to
    2. HashSet(Collection<? extends E>) as a member of raw type HashSet
    3. Set result = new HashSet(s1);
    4. ^
    5. Union.java:6: warning: [unchecked] unchecked call to
    6. addAll(Collection<? extends E>) as a member of raw type Set
    7. result.addAll(s2);
    8. ^

      要修复这些警告并使方法类型安全,请修改其声明以声明表示三个集合(两个参数和返回值)的元素类型的类型参数,并在整个方法中使用此类型参数。 声明类型参数的类型参数列表位于方法的修饰符和返回类型之间。 在这个例子中,类型参数列表是 <E>,返回类型是 Set<E>。 类型参数的命名约定对于泛型方法和泛型类型是相同的(详见第 29 和 68 条):

    1. // Generic method
    2. public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    3. Set<E> result = new HashSet<>(s1);
    4. result.addAll(s2);
    5. return result;
    6. }

      至少对于简单的泛型方法来说,就是这样。 此方法编译时不会生成任何警告,并提供类型安全性和易用性。 这是一个简单的程序来运行该方法。 这个程序不包含强制转换和编译时没有错误或警告:至少对于简单的泛型方法来说,就是这样。 此方法编译时不会生成任何警告,并提供类型安全性和易用性。 这是一个简单的程序来运行该方法。 这个程序不包含强制转换和编译时没有错误或警告:

      当运行这个程序时,它会打印[Moe, Tom, Harry, Larry, Curly, Dick](输出中元素的顺序依赖于具体实现。)

      有时,需要创建一个不可改变但适用于许多不同类型的对象。 因为泛型是通过擦除来实现的(详见第 28 条),所以可以使用单个对象进行所有必需的类型参数化,但是需要编写一个静态工厂方法来重复地为每个请求的类型参数化分配对象。 这种称为泛型单例工厂(generic singleton factory)的模式用于方法对象(function objects)(详见第 42 条),比如 Collections.reverseOrder 方法,偶尔也用于 Collections.emptySet 之类的集合。

      假设你想写一个恒等方法分配器( identity function dispenser)。 类库提供了 Function.identity 方法,所以没有理由编写你自己的实现(详见第 59 条),但它是有启发性的。 如果每次要求的时候都去创建一个新的恒等方法对象是浪费的,因为它是无状态的。 如果 Java 的泛型被具体化,那么每个类型都需要一个恒等方法,但是由于它们被擦除以后,所以泛型的单例就足够了。 以下是它的实例:

    1. @SuppressWarnings("unchecked")
    2. public static <T> UnaryOperator<T> identityFunction() {
    3. return (UnaryOperator<T>) IDENTITY_FN;
    4. }

      将 IDENTITY_FN 转换为 (UnaryFunction<T>) 会生成一个未经检查的强制转换警告,因为 UnaryOperator<Object> 对于每个 T 都不是一个 UnaryOperator<T>。但是恒等方法是特殊的:它返回未修改的参数,所以我们知道,使用它作为一个 UnaryFunction<T> 是类型安全的,无论 T 的值是多少。因此,我们可以放心地抑制由这个强制生成的未经检查的强制转换警告。 一旦我们完成了这些,代码编译没有错误或警告。

      下面是一个示例程序,它使用我们的泛型单例作为 UnaryOperator<String>UnaryOperator<Number>。 像往常一样,它不包含强制转化,编译时也没有错误和警告:

    1. // Sample program to exercise generic singleton
    2. public static void main(String[] args) {
    3. String[] strings = { "jute", "hemp", "nylon" };
    4. UnaryOperator<String> sameString = identityFunction();
    5. for (String s : strings)
    6. System.out.println(sameString.apply(s));
    7. Number[] numbers = { 1, 2.0, 3L };
    8. for (Number n : numbers)
    9. System.out.println(sameNumber.apply(n));
    10. }

      虽然相对较少,类型参数受涉及该类型参数本身的某种表达式限制是允许的。 这就是所谓的递归类型限制(recursive type bound)。 递归类型限制的常见用法与 Comparable 接口有关,它定义了一个类型的自然顺序(详见第 14 条)。 这个接口如下所示:

      许多方法采用实现 Comparable 的元素的集合来对其进行排序,在其中进行搜索,计算其最小值或最大值等。 要做到这一点,要求集合中的每一个元素都可以与其中的每一个元素相比,换言之,这个元素是可以相互比较的。 以下是如何表达这一约束:

    1. // Using a recursive type bound to express mutual comparability
    2. public static <E extends Comparable<E>> E max(Collection<E> c);

      限定的类型 <E extends Comparable <E >> 可以理解为「任何可以与自己比较的类型 E」,这或多或少精确地对应于相互可比性的概念。

      这里有一个与前面的声明相匹配的方法。它根据其元素的自然顺序来计算集合中的最大值,并编译没有错误或警告:

    1. // Returns max value in a collection - uses recursive type bound
    2. public static <E extends Comparable<E>> E max(Collection<E> c) {
    3. if (c.isEmpty())
    4. throw new IllegalArgumentException("Empty collection");
    5. E result = null;
    6. for (E e : c)
    7. if (result == null || e.compareTo(result) > 0)
    8. result = Objects.requireNonNull(e);
    9. }

      请注意,如果列表为空,则此方法将引发 异常。 更好的选择是返回一个 Optional<E>(详见第 55 条)。

      递归类型限制可能变得复杂得多,但幸运的是他们很少这样做。 如果你理解了这个习惯用法,它的通配符变体(详见第 31 条)和模拟的自我类型用法(详见第 2 条),你将能够处理在实践中遇到的大多数递归类型限制。